/**
 * 
 */
package com.goldentrail.dml;

import java.util.ArrayList;
import java.util.Map.Entry;

import com.goldentrail.dataobject.DataArtifact;
import com.goldentrail.dataobject.Invocation;
import com.goldentrail.dataobject.Node;
import com.goldentrail.dataobject.Run;
import com.goldentrail.neo4j.Neo4jNode;
import com.goldentrail.neo4j.Relationship;
import com.goldentrail.neo4j.TraverserSpec;
import com.goldentrail.neo4j.RelationshipSpec;

/**
 * contains static method for adding Runs to a Neo4j database. The runs are to
 * be described in com.goldentrail.dataobject.Run objects
 * 
 * @author mikeagun
 * 
 */
public class Neo4jBuilder {
	public static final String FINAL_OUTPUT_REL = "FINAL_OUTPUT";
	public static final String FINAL_OUTPUT_POS = "final_output";
	public static final String INIT_INPUT_REL = "INIT_INPUT";
	public static final String INIT_INPUT_POS = "initial_input";
	public static final String INTERMEDIATE_POS = "intermediate";
	public static final String RUN_TYP = "run";
	public static final String INVOC_TYP = "invoc";
	public static final String DATA_TYP = "data";
	public static final String RUN_INVOC_REL = "RUN_INVOC";
	public static final String RUN_DATA_REL = "RUN_DATA";
	public static final String ROOT_RUN_REL = "ROOT_RUN";
	public static final String GEN_BY_REL = "GEN_BY";
	public static final String USED_REL = "USED";
	public static final String SAME_VALUE_REL = "SAME_VALUE";

	/**
	 * Adds a Run to the Neo4jDatabase. requires no setup other than
	 * com.goldentrail.neo4j.URIs.SERVER_ROOT_URI being correct
	 * 
	 * @param r
	 *            the run to add to the database
	 */
	public static void AddToDB(Run r) {
		if (Neo4jNode.getNode("run", "id", r.getRunId()) == null)
		{
			//System.out.println("In AddToDB():Start");
			Neo4jNode dbRun = Neo4jNode.createNode(r.getJSONString());
			//so we can easily get a list of runs
			dbRun.addRelationship(Neo4jNode.getNode(0), ROOT_RUN_REL);
			dbRun.indexNode("run", "id", r.getRunId());
	
			for (Invocation n : r.getInvocations()) {
				// ArrayList<Neo4jNode> invocNodes = new ArrayList<Neo4jNode>();
				/* invocNodes.add( */addToDB(n, dbRun, r.getRunId())/* ) */;
			}
			// now find init inputs and final outputs
			setDataRels(dbRun);
			//System.out.println("In AddToDB():Finished");
		}
		else
			System.out.println("Run already exists, nothing done.");
	}

	/**
	 * Generates Final output and initial input relations for all the data of a
	 * given run
	 * 
	 * @param runNode
	 *            the Neo4jNode connected to the node for the run that is to be
	 *            processed
	 */
	private static void setDataRels(Neo4jNode runNode) {
		ArrayList<Relationship> dataRels = runNode.getRelationships("in",
				RUN_DATA_REL);
		for (Relationship dataRel : dataRels) {
			Neo4jNode dbData = dataRel.getStartNode();
			ArrayList<Relationship> usedRels = dbData.getRelationships("in",
					USED_REL);
			ArrayList<Relationship> genByRels = dbData.getRelationships("out",
					GEN_BY_REL);
			if (genByRels.size() < 1) // has no gen by, must be initial input
				runNode.addRelationship(dbData, INIT_INPUT_REL);
			else if (usedRels.size() < 1) // never used, must be final output
				runNode.addRelationship(dbData, FINAL_OUTPUT_REL);
			else {
				// if connected to 2+ runs, must be final output and initial
				// input
				if (dbData.getRelationships("out", RUN_DATA_REL).size() > 1) {
					// get the runs that this node is a final output/initial
					// input of
					ArrayList<Relationship> runRels = dbData.getRelationships(
							"in", FINAL_OUTPUT_REL, INIT_INPUT_REL);
					ArrayList<Neo4jNode> runs = new ArrayList<Neo4jNode>();
					for (Relationship runRel : runRels) {
						runs.add(runRel.getEndNode());
					}
					Neo4jNode genRun = genByRels.get(0).getEndNode()
							.getRelationships("out", RUN_INVOC_REL).get(0)
							.getEndNode();
					// set data as final output of generating run if not already
					if (!runs.contains(genRun))
						genRun.addRelationship(dbData, FINAL_OUTPUT_REL);

					// set data as init input of all using runs (as needed)
					for (Relationship rel : usedRels) {
						// get run of node that is using this data
						Neo4jNode usingRun = rel.getStartNode()
								.getRelationships("out", RUN_INVOC_REL).get(0)
								.getEndNode();
						if (!runs.contains(usingRun))
							usingRun.addRelationship(dbData, INIT_INPUT_REL);
					}

				}
			}
		}
	}

	/**
	 * Adds a DataArtifact to the database. returns reference if already exists. also adds SAME_VALUE relationships to other data artifacts with the same value to help in manual stitching
	 * 
	 * @param data
	 *            DataArtifact to add to the database
	 * @return Neo4jNode that is connected to the node object that has been
	 *         created
	 */
	private static Neo4jNode addToDB(DataArtifact data, Neo4jNode runNode) {
		ArrayList<Neo4jNode> nodes = Neo4jNode.getNodes("data", "id",
				data.getDataArtifactId());
		Neo4jNode dbData = null;
		if (nodes != null) {
			for (Neo4jNode node : nodes) {
				if (node.getProperty("value").equals(data.getValue())) {
					dbData = node;
					break;
				}
			}
		}

		if (dbData == null) {
			dbData = Neo4jNode.createNode(data.getJSONString());
			dbData.addRelationship(runNode, RUN_DATA_REL);
			dbData.indexNode("data", "id", data.getDataArtifactId());
			ArrayList<Neo4jNode> valueNodes = Neo4jNode.getNodes("data",
					"value", data.getValue());

			// add same value rel to other nodes with same value but diff id
			for (Neo4jNode valueNode : valueNodes)
				dbData.addRelationship(valueNode, SAME_VALUE_REL);

			// now index so we can connect to future nodes with same value
			dbData.indexNode("data", "value", data.getValue());
		} else {
			ArrayList<Relationship> rels = dbData.getRelationships("out",
					RUN_DATA_REL);
			for (Relationship rel : rels) {
				if (rel.getEndNode().equals(runNode))
					return dbData;
			}
			dbData.addRelationship(runNode, RUN_DATA_REL);
		}

		return dbData;
	}

	/**
	 * Adds an Invocation to the database, using the specified Neo4jNode as the
	 * run that is to contain the node. returns reference if already exists.
	 * 
	 * @param invoc
	 *            Invocation to add to the database
	 * @param runNode
	 *            Neo4jNode connected to the run node in the database
	 * @return Neo4jNode that is connected to the node object that has been
	 */
	private static Neo4jNode addToDB(Invocation invoc, Neo4jNode runNode,
			String runId) {
		ArrayList<Neo4jNode> nodes = Neo4jNode.getNodes("invoc", "id",
				invoc.getInvocationId());
		if (nodes != null) {
			for (Neo4jNode node : nodes) {
				if (node.getRelationships("out", RUN_INVOC_REL).get(0).getEndNode()
						.equals(runNode)) {
					return node;
				}
	
			}
		}

		Neo4jNode dbNode = Neo4jNode.createNode(invoc.getJSONString());
		dbNode.addProperty("run", runId);

		dbNode.indexNode("invoc", "id", invoc.getInvocationId());

		// now connect this node to its input and output data
		for (Entry<String, DataArtifact> entry : invoc.getInputData()
				.entrySet()) {
			DataArtifact data = entry.getValue();
			Neo4jNode dbData = addToDB(data, runNode);
			dbNode.addRelationship(dbData, USED_REL);
		}
		for (Entry<String, DataArtifact> entry : invoc.getOutputData()
				.entrySet()) {
			DataArtifact data = entry.getValue();
			Neo4jNode dbData = addToDB(data, runNode);
			dbData.addRelationship(dbNode, GEN_BY_REL);
		}

		dbNode.addRelationship(runNode, RUN_INVOC_REL);

		return dbNode;
	}
}
